home *** CD-ROM | disk | FTP | other *** search
/ PC World Komputer 2010 April / PCWorld0410.iso / pluginy Firefox / 7661 / 7661.xpi / components / RILsync.js < prev    next >
Text File  |  2009-12-23  |  20KB  |  671 lines

  1. /*
  2.  
  3. License: This source code may not be used in other applications whether they
  4. be personal, commercial, free, or paid without written permission from Read It Later.
  5.  
  6.  
  7. /////////
  8. DEVELOPER API: readitlaterlist.com/api/
  9. /////////
  10.  
  11. If you would like to customize Read It Later or build an application that works with
  12. Read it Later take a look at the READ IT LATER OPEN API:
  13. http://readitlaterlist.com/api/
  14.  
  15. Suggestions for additions to Read It Later are VERY welcome.  A large number of user
  16. suggestions have been implemented.  Please let me know of any additional features you
  17. are seeking at: http://readitlaterlist.com/support/
  18.  
  19. Thanks
  20.  
  21. */
  22.  
  23.  
  24. Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
  25.  
  26. function RILsync()
  27. {
  28.     this.wrappedJSObject = this;
  29.     
  30.     this.batch = [];
  31.     this.requests = {};
  32.     
  33.     this.waitBeforeSending         = 2 * 1000;
  34.     
  35. }
  36.  
  37. // class definition
  38. RILsync.prototype = {
  39.  
  40.   // properties required for XPCOM registration:
  41.   classDescription: "Read It Later Sync Javascript XPCOM Component",
  42.   classID:          Components.ID("{d12be5c0-aaae-11de-8a39-0800200c9a66}"),
  43.   contractID:       "@ril.ideashower.com/rilsync;1",
  44.  
  45.   QueryInterface: XPCOMUtils.generateQI(),
  46.  
  47.  ////////////////////////////////////////////////
  48.     
  49.     
  50.     init : function()
  51.     { 
  52.         this.APP    = Components.classes['@ril.ideashower.com/rildelegate;1'].getService().wrappedJSObject;
  53.         this.LIST   = this.APP.LIST;
  54.         this.PREFS  = this.APP.PREFS;
  55.         this.JSON   = Components.classes["@mozilla.org/dom/json;1"].createInstance(Components.interfaces.nsIJSON);
  56.         
  57.         this.APP.registerObserver('ril-api-request-finished');
  58.     },
  59.     
  60.      
  61.     // -- Sync Queue -- //
  62.     
  63.     // Add to sync queue
  64.     addToSyncQueue : function(type, url, batch, delay)
  65.     {    
  66.     if (!this.syncingEnabled()) return false;
  67.     
  68.     let statement = this.APP.DB.createStatement("REPLACE INTO sync_queue (type, url) VALUES (:type, :url)");
  69.     statement.params.type = type;
  70.     statement.params.url = url;
  71.     this.batch.push( statement );
  72.     this.changeReadyForServer = delay && !this.changeReadyForServer ? false : true;
  73.         
  74.         if (!batch) {
  75.             this.flushBatch();
  76.         }    
  77.     },
  78.     
  79.     // Remove from sync queue
  80.     removeFromSyncQueue : function(type, url, batch)
  81.     {
  82.     let statement = this.APP.DB.createStatement("DELETE FROM sync_queue WHERE type = :type AND url = :url");
  83.     statement.params.type = type;
  84.     statement.params.url = url;
  85.     this.batch.push( statement ); 
  86.     this.changeReadyForServer = true;    
  87.         
  88.         if (!batch) {
  89.             this.flushBatch();
  90.         }    
  91.     },
  92.     
  93.     // Clear sync queue
  94.     clearSyncQueue : function(fromLastRowId)
  95.     {
  96.     if (!fromLastRowId) fromLastRowId = 1000000;
  97.     let statement = this.APP.DB.createStatement("DELETE FROM sync_queue WHERE rowid <= :rowId");
  98.     statement.params.rowId = fromLastRowId;
  99.     this.batch.push( statement );     
  100.         this.flushBatch();    
  101.     },
  102.     
  103.     
  104.     // -- Sync -- //
  105.     
  106.     syncingEnabled : function()
  107.     {
  108.     return this.APP.getLogin();
  109.     },
  110.     
  111.     cancelSync : function()
  112.     {
  113.         this.syncing = false;
  114.         this.sending = false;
  115.         this.getting = false;
  116.     this.waitingToGet = false;
  117.     this.waitingToHardSync = false;
  118.         this.syncWasCancelled = true;
  119.     this.APP.refreshListInAllOpenWindows('list');
  120.     },
  121.     
  122.     sync : function(hard, manual) {
  123.     
  124.     if (!this.syncingEnabled())
  125.     {
  126.         this.APP.commandInTopRIL('switchToList', 'list');
  127.         this.APP.genericMessage("You need an account to sync your list with other computers and devices.", [{label:'Register',delegate:this.APP.getTopRIL(),selector:'openLogin'},
  128.          {label:'Log-in',delegate:this.APP.getTopRIL(),selector:'openLogin'}],
  129.             false, 'Sync', true);
  130.         return false;
  131.     }
  132.     
  133.     if (this.syncing)
  134.     {        
  135.         // If user clicks 'sync' while it's running in bg, nothing would happen unless we turn off background process
  136.         if (manual && this.syncInBackgroundTillResults)
  137.         {
  138.         this.syncInBackgroundTillResults = false;
  139.         this.APP.refreshListInAllOpenWindows('list');
  140.         }
  141.         
  142.         return false;
  143.     }
  144.     this.syncing = true;
  145.         this.syncWasCancelled = false;
  146.     this.APP.commandInTopRIL('switchToList', 'list');    
  147.     this.APP.refreshListInAllOpenWindows('list');
  148.     
  149.     this.waitingToGet = true;
  150.     this.waitingToHardSync = hard;
  151.     
  152.     this.send(true); //if this returns false, it's okay because we just set waitingToGet to be true
  153.     },    
  154.     
  155.     // Sync - Send
  156.     // Sending is done on the main thread to prevent anything from changing while we're retrieving the queue, etc
  157.     send : function(showErrors) {        
  158.     
  159.     if (!this.syncingEnabled()) return false;
  160.     
  161.     try {
  162.     if (this.sending) return false;
  163.     this.sending = true;
  164.     this.delaySend = 0;
  165.         this.APP.clearTimeout(this.syncChangesTO);
  166.         this.syncChangesTO = null;
  167.     
  168.         // Get sync queue               
  169.     let sql, statement, row, item, i;        
  170.     this.lastRowId = 0;
  171.     
  172.     // Retrieve Syncing Queue
  173.     let newQueue = [];
  174.     let readQueue = [];
  175.     let deleteQueue = [];
  176.     let titleQueue = [];
  177.     let tagsQueue = [];
  178.     let scrollQueue = [];
  179.     sql = "SELECT rowid, type, url FROM sync_queue";    
  180.     statement = this.APP.DB.createStatement(sql);
  181.     try {
  182.         while (statement.step())
  183.         {             
  184.         row = statement.row;
  185.         this.lastRowId = this.lastRowId < row.rowid ? row.rowid : this.lastRowId;
  186.         
  187.         item = this.LIST.itemByUrl( row.url );
  188.         if (!item && (row.type != 'delete' && row.type != 'read')) continue;
  189.         
  190.         switch( row.type )
  191.         {
  192.             case('new'):
  193.             newQueue.push( {
  194.                 url: this.APP.e(item.url),
  195.                 title: this.APP.et(item.title)
  196.             } );
  197.             break;
  198.             
  199.             case('read'):
  200.             readQueue.push( {
  201.                 url: this.APP.e(row.url)
  202.             } );
  203.             break;
  204.             
  205.             case('delete'):
  206.             deleteQueue.push( {
  207.                 url: this.APP.e(row.url)
  208.             } );
  209.             break;
  210.             
  211.             case('title'):
  212.             titleQueue.push( {
  213.                 url: this.APP.e(item.url),
  214.                 title: this.APP.et(item.title)
  215.             } );
  216.             break;
  217.             
  218.             case('tags'):
  219.             tagsQueue.push( {
  220.                 url: this.APP.e(item.url),
  221.                 tags: this.APP.et(item.tagList)
  222.             } );
  223.             break;
  224.             
  225.             case('scroll'):
  226.             scrollQueue.push( {
  227.                 url: this.APP.e(item.url),
  228.                 views: item.scroll
  229.             } );
  230.             break;
  231.             
  232.         }
  233.         
  234.         } 
  235.     }
  236.     catch(e) {
  237.         Components.utils.reportError(e);
  238.     }
  239.     finally {
  240.         statement.reset();
  241.     }
  242.     
  243.     // Clear list
  244.     if (this.syncBatchItems)
  245.         delete this.syncBatchItems;    
  246.         
  247.     // Anything to send?
  248.     if ( newQueue.length ||
  249.          readQueue.length ||
  250.          deleteQueue.length ||
  251.          titleQueue.length ||
  252.          tagsQueue.length ||
  253.          scrollQueue.length)
  254.     {
  255.         
  256.         // Create Parameter string
  257.         let params = '';
  258.         
  259.         if (newQueue.length)
  260.         params += '&new=' + this.JSON.encode( newQueue );
  261.         
  262.         if (readQueue.length)
  263.         params += '&read=' + this.JSON.encode( readQueue );
  264.         
  265.         if (deleteQueue.length)
  266.         params += '&delete=' + this.JSON.encode( deleteQueue );
  267.         
  268.         if (titleQueue.length)
  269.         params += '&update_title=' + this.JSON.encode( titleQueue );
  270.         
  271.         if (tagsQueue.length)
  272.         params += '&update_tags=' + this.JSON.encode( tagsQueue );
  273.         
  274.         if (scrollQueue.length)
  275.         params += '&position=' + this.JSON.encode( scrollQueue );
  276.             
  277.         
  278.         // If manually syncing or doing a hard sync, use immediate flush
  279.         params += '&' + 'immediate=1';
  280.                 
  281.         // Create connection
  282.             this.request( 'send' , true, params, this, 'sendCallback', showErrors ? null : 'none');
  283.         
  284.     } else {
  285.         this.syncing = false; //TODO is this right here?
  286.         this.sending = false;
  287.         
  288.         if (this.waitingToGet) this.get(true);    
  289.     }
  290.     
  291.     } catch(e) { Components.utils.reportError(e); }
  292.         
  293.     },
  294.     
  295.     sendCallback : function(request) {
  296.         try {
  297.     this.syncing = false;
  298.     this.sending = false;
  299.     if (request.success && !this.syncWasCancelled) {
  300.         
  301.         this.clearSyncQueue(this.lastRowId);
  302.         if (this.waitingToGet) this.get(true);
  303.         
  304.     } else {
  305.         this.APP.refreshListInAllOpenWindows(); // genericMessage should be in place now... this may not be needed
  306.     }
  307.         this.APP.OBSERVER.notifyObservers(null, 'ril-api-send-finished', request.success);
  308.         } catch(e) { Components.utils.reportError(e); }
  309.     },
  310.     
  311.     
  312.     // Sync - Get
  313.     get : function(fromSync) {
  314.     
  315.     if (!this.syncingEnabled()) return false;
  316.     
  317.     if (fromSync && this.getting) return false; // can allow multiple gets?
  318.         this.syncing = fromSync ? true : this.syncing;
  319.     this.getting = true;
  320.     this.waitingToGet = false;
  321.     
  322.     // Params
  323.     let since = this.waitingToHardSync ? '' : this.PREFS.get('since');
  324.     this.waitingToHardSync = false;    
  325.     
  326.     let params = 'since='+since+'&tags=1&positions=1';
  327.     
  328.     // if the user's reading list is empty, then we have no use for their read list, so save the cycles and only request unread
  329.     if (this.LIST.list.length == 0)
  330.     params += '&state=unread';
  331.     
  332.     // Create connection
  333.         this.request('get', true, params, this, 'getCallback');
  334.     
  335.     },
  336.     
  337.     getCallback : function(request) {
  338.     try {
  339.     
  340.     // If there are some results, process these in a thread
  341.     let newItems = [];
  342.     let itemId;
  343.     if (request.success && !this.syncWasCancelled)
  344.     {
  345.         //this.APP.d(request.response);
  346.         let response = this.JSON.decode( request.response );
  347.         let compare = false;
  348.         let compareItem;
  349.  
  350.         if (response.complete)
  351.         {
  352.         // full list returned - hard sync
  353.         // compare current list to this list and send back any items that are saved locally
  354.         // but not on the server
  355.         // first we'll get a copy of the list
  356.         // then as we go through the list below, we remove any items that we have a record of (read/unread)
  357.         // finally after the synced items are run through, we'll go through what's remaining of the compare list
  358.         // and send those back to the server
  359.         compare = this.LIST.list.slice();
  360.         }
  361.         
  362.         if (response.status == 1 && response.list)
  363.         {
  364.         // There is some data in the response
  365.         // This may benefit from being in a thread, but it will be complicated
  366.         // making RILlist's remove/add/update functions threadsafe
  367.         
  368.         if (this.LIST.syncInBackgroundTillResults)
  369.             this.APP.refreshList('list');
  370.         
  371.         
  372.         let getItem, i, localItem, getUrl;
  373.         for(let n in response.list)
  374.         {
  375.             getItem = response.list[n];
  376.             localItem = this.LIST.itemByUrl( getItem.url );
  377.                         
  378.             if (getItem.state == 1)
  379.             {
  380.             if (localItem) {
  381.                 this.LIST.mark(localItem.itemId, true, true);
  382.                 
  383.                 if (compare)
  384.                 {
  385.                 compareItem = compare[ this.LIST.iByItemId[localItem.itemId] ];
  386.                 if (compareItem)
  387.                 {
  388.                     compareItem.compare = true;
  389.                 }                
  390.                 }
  391.                 
  392.             } else {
  393.                 // nothing to do
  394.             }            
  395.             
  396.             }
  397.             else
  398.             { //unread
  399.             if (localItem)
  400.             {
  401.                 //this.APP.d('----------')
  402.                 //this.APP.d( this.APP.ar(localItem, true) )
  403.                 
  404.                 if (getItem.item_id != localItem.itemId) {
  405.                 //this.LIST.updateItemId(localItem.itemId, getItem.item_id);
  406.                 }
  407.                 if (getItem.title != localItem.title) {
  408.                 this.LIST.saveTitle(localItem.itemId, getItem.title, true, true);
  409.                 }
  410.                 
  411.                 if (getItem.time_added != localItem.timeUpdated) {
  412.                 this.LIST.updateTimeUpdated(localItem.itemId, getItem.time_added, true);
  413.                 }
  414.                 if (getItem.tags != localItem.tagList) {
  415.                 this.LIST.compareAndUpdateTags(localItem.itemId, getItem.tags, localItem.tagList, true);
  416.                 }
  417.                 if (getItem.position) {                
  418.                                 this.LIST.updateScrollPositions(localItem.itemId, getItem.position, true, true);                                
  419.                 }
  420.                 
  421.                 if (compare)
  422.                 {
  423.                 compareItem = compare[ this.LIST.iByItemId[localItem.itemId] ];
  424.                 if (compareItem)
  425.                 {
  426.                     compareItem.compare = true;
  427.                 }                
  428.                 }
  429.             }
  430.             else
  431.             {
  432.                 if (this.APP.checkIfValidUrl(getItem.url))
  433.                 {
  434.                 
  435.                 try
  436.                 {
  437.                     itemId = this.LIST.add({
  438.                     itemId: getItem.item_id,
  439.                     url: getItem.url,
  440.                     title: getItem.title,
  441.                     timeUpdated: getItem.time_added,
  442.                     tagList: getItem.tags ? getItem.tags : false,
  443.                     positions : getItem.position ? getItem.position : false
  444.                     }, true, true);
  445.                     newItems.push(itemId);
  446.                 }
  447.                 catch(e) { Components.utils.reportError(e); }
  448.                 
  449.                 }
  450.             }
  451.             }
  452.             
  453.         }
  454.         }
  455.         
  456.         // handle remaining comparison list
  457.         for(i in compare)
  458.         {
  459.         if (!compare[i].compare && compare[i].url)
  460.         {
  461.                     
  462.                     // remove all matches of url - is this a problem moving forward or just b1, b2 testers? - import should still do url check to prevent dupes
  463.                     // if there are dupes in the list (because of old versions that parsed urls different, for example
  464.                     // 2.0 now compares #anchors as the same link, it needs to make sure that it was not skipped over)
  465.                     // we could add a dupe check function that removes items that are now considered duplicates, but
  466.                     // that seems like it could have a lot of pitfalls and corner cases that might cause false positives for users
  467.             //localItem = this.LIST.itemByUrl( compare[i].url );
  468.             //if (!localItem)
  469.             //{
  470.                     //    this.APP.d('add: ' + compare[i].url);
  471.             this.addToSyncQueue('new', compare[i].url, true);
  472.             //}
  473.         }
  474.         }
  475.         
  476.         // update sync time
  477.         this.PREFS.set('since', response.since);
  478.         
  479.     }
  480.     
  481.         
  482.     this.syncing = false;
  483.     this.getting = false;    
  484.     this.syncInBackgroundTillResults = false;
  485.     
  486.     if (request.success)
  487.     {
  488.         this.LIST.endBatchAndRefresh();
  489.         
  490.         if (this.PREFS.getBool('autoOffline') && newItems)
  491.         this.APP.updateOfflineQueue(newItems);
  492.     }
  493.     
  494.     } catch(e) {
  495.         Components.utils.reportError(e);
  496.         this.APP.genericMessage('There was an error while syncing:\n'+e,
  497.                     [
  498.                     {label:'Try Again', delegate:this, selector:'sync'},
  499.                     {label:'Get Help', delegate:this.APP.getTopRIL(), selector:'getHelp'}
  500.                     ], false, 'Sync', false);
  501.     }       
  502.     },
  503.     
  504.     
  505.     // -- Read List -- //
  506.     
  507.     getReadList : function(page, filter, sort, count, delegate, noCache)
  508.     {        
  509.     if (!this.syncingEnabled()) return false;
  510.     //if (this.syncingRead) return false; // allow overlaps? // TODO: cancel other request - per window?
  511.     delegate.syncingRead = true;
  512.     
  513.         // If there are pending sync changes, those have to be sent first before reloading list
  514.         if (this.syncChangesTO)
  515.         {
  516.             this.APP.registerObserver('ril-api-send-finished', delegate);
  517.             this.send();
  518.             return;
  519.         }
  520.         
  521.         
  522.     let page = page ? page : 1;
  523.     
  524.         // TODO set readFetchCount to use perPage setting and add count limit to api
  525.         let params = 'format=json&state=read&count='+count+'&page='+page;
  526.         if (filter) params += '&search='+filter;
  527.         if (sort) params += '&sort='+sort;
  528.         if (noCache) params += '&nocache=1';
  529.         
  530.         this.request('search', true, params, delegate, 'readCallback', null, 'read');
  531.     },
  532.     
  533.     readCallback : function(request) {
  534.     try {
  535.             
  536.         let readList = [];
  537.         let iByReadItemId = {};
  538.         let total = 0;
  539.         
  540.     if (request.success)
  541.     {
  542.         let response = this.JSON.decode( request.response );
  543.         let c = 0;
  544.         
  545.         if (response.status == 1 && response.list)
  546.         {
  547.         for(let n in response.list)
  548.         {
  549.             getItem = response.list[n];
  550.             
  551.             if (this.APP.checkIfValidUrl(getItem.url))
  552.             {        
  553.             
  554.             iByReadItemId[ getItem.item_id ] = readList.length;
  555.             
  556.             readList.push( {
  557.                 itemId      : getItem.item_id,
  558.                 uniqueId    : getItem.item_id,
  559.                 url         : getItem.url,
  560.                 title       : getItem.title,
  561.                 timeUpdated : getItem.time_updated
  562.                 } );
  563.             
  564.             }
  565.             c++; // outside of the loop because we still want to know if there were a full set of items even if some were invalid
  566.             
  567.         }
  568.                 
  569.                 total = response.total;
  570.         }
  571.         
  572.         // No more items to get
  573.         this.noMoreReadItems = (c < this.readFetchCount);
  574.         
  575.     } else {
  576.         readList = null;
  577.     }
  578.         
  579.         return {list:readList, iByItem:iByReadItemId, total:total};
  580.     
  581.     } catch(e) {Components.utils.reportError(e);}        
  582.     },
  583.     
  584.     // -- //
  585.     
  586.     deleteRemote: function(url, batch)
  587.     {
  588.         this.addToSyncQueue( 'delete', url, batch);
  589.         
  590.         this.APP.LIST.readListNeedsRefresh();
  591.         
  592.         if (!batch) {
  593.             this.APP.LIST.endBatchAndRefresh();
  594.         }
  595.     },
  596.     
  597.     
  598.     // -- Auth -- //
  599.     
  600.     login : function(username, password, delegate, selector)
  601.     {
  602.         this.request('auth', false, 'username='+this.APP.e(username)+'&password='+this.APP.e(password), delegate, selector, 'none');
  603.     },
  604.     
  605.     signup : function(username, password, delegate, selector)
  606.     {
  607.         this.request('signup', false, 'username='+this.APP.e(username)+'&password='+this.APP.e(password), delegate, selector, 'none');
  608.     },
  609.     
  610.     
  611.     // Requests
  612.     
  613.     request : function(method, login, params, delegate, selector, errorReporting, methodDescription)
  614.     {    
  615.     if (this.APP.listError)
  616.     {
  617.         this.APP.genericMessage("Because Read It Later failed to load correctly, syncing has been disabled to prevent any loss of data.\n\nTry restarting Firefox or clicking 'Get Help'.",
  618.                     [
  619.                     {label:'Get Help', delegate:this.APP.getTopRIL(), selector:'getHelp'}
  620.                     ], false, false, true);
  621.         return false;
  622.     }
  623.         
  624.         let requestSet          = {delegate:delegate, selector:selector};
  625.         requestSet.request      = Components.classes['@ril.ideashower.com/rilapirequest;1'].createInstance(Components.interfaces.nsIRILAPIRequest);
  626.         let requestId           = requestSet.request.initAndStart(method, true, params, errorReporting ? errorReporting : 'all', methodDescription);
  627.         this.requests[requestId] = requestSet;
  628.     },
  629.     
  630.     requestCallback : function(apiRequest, requestId)
  631.     {
  632.         let requestSet = this.requests[ requestId ];
  633.         if (requestSet)
  634.         {            
  635.             let request = requestSet.request;            
  636.             requestSet.delegate[ requestSet.selector ]( request, request.success, request.response );
  637.             delete requestSet;
  638.         }
  639.     },
  640.     
  641.     
  642.     
  643.     //  -- 
  644.     
  645.     flushBatch : function(callback) {
  646.     // grab a snapshot of the batch and then clear it
  647.     let batch = this.batch.slice();
  648.     this.batch = [];
  649.     
  650.     let sizeOfBatch = batch.length;
  651.         if (batch.length > 0) {            
  652.             this.APP.DB.executeAsync( batch , batch.length, null );
  653.         }
  654.     
  655.     if (sizeOfBatch > 0 && this.changeReadyForServer) {
  656.         this.changeReadyForServer = false;
  657.                 
  658.         this.APP.clearTimeout(this.syncChangesTO);
  659.         this.syncChangesTO = this.APP.setTimeout(this.send, this.delaySend==1 ? this.delayedWaitBeforeSending : this.waitBeforeSending, this);
  660.     }
  661.     }
  662.  
  663. };
  664.  
  665.  
  666.  
  667. var components = [RILsync];
  668. function NSGetModule(compMgr, fileSpec) {
  669.   return XPCOMUtils.generateModule(components);
  670. }
  671.